﻿#region File and License Information
/*
<File>
	<License Type="BSD">
		Copyright © 2009 - 2012, Outcoder. All rights reserved.
	
		This file is part of Calcium (http://calciumsdk.net).

		Redistribution and use in source and binary forms, with or without
		modification, are permitted provided that the following conditions are met:
			* Redistributions of source code must retain the above copyright
			  notice, this list of conditions and the following disclaimer.
			* Redistributions in binary form must reproduce the above copyright
			  notice, this list of conditions and the following disclaimer in the
			  documentation and/or other materials provided with the distribution.
			* Neither the name of the <organization> nor the
			  names of its contributors may be used to endorse or promote products
			  derived from this software without specific prior written permission.

		THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
		ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
		WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
		DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
		DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
		(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
		LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
		ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
		(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
		SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
	</License>
	<Owner Name="Daniel Vaughan" Email="danielvaughan@outcoder.com" />
	<CreationDate>2010-07-30 17:32:22Z</CreationDate>
</File>
*/
#endregion

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Xml.Linq;

using Outcoder.Collections;
using Outcoder.ComponentModel;
using Outcoder.Concurrency;
using Outcoder.Data;
using Outcoder.Data.Serialization;
using Outcoder.SettingsModel;

namespace Outcoder.Services.Implementation
{
	/// <summary>
	/// This is the default implementation of the ISettingsService.
	/// To change where the service stores and retrieves its values,
	/// provide a custom <see cref="ISettingsStore"/> implementation.
	/// For Silverlight and Windows Phone, the <c>ISettingsStore</c>
	/// uses Isolated Storage.
	/// </summary>
	public partial class SettingsService : ISettingsService
	{
		readonly SettingsEventBroadcaster eventBroadcaster;
		readonly ISettingsStore localStore;
		readonly ISettingsStore transientStore;
		readonly ISettingsStore roamingStore;
		const string dateTimeFormatString = "yyyy-MM-ddTHH:mm:ss.fffffffzzz";

		public bool RaiseExceptionsOnConversionErrors { get; set; }

		public SettingsService(ISettingsStore localStore = null, 
			ISettingsStore roamingStore = null, ISettingsStore transientStore = null)
		{
			this.localStore = localStore ?? new LocalSettingsStore();

#if NETFX_CORE || WP81
			this.roamingStore = roamingStore ?? new RoamingSettingsStore();
#else
			this.roamingStore = roamingStore;
#endif

#if NETFX_CORE
			this.transientStore = transientStore 
				?? new TransientSettingsStore(new ComponentModel.StatePreservation.TransientState());
#else
			this.transientStore = transientStore ?? new TransientSettingsStore();
#endif

			eventBroadcaster = new SettingsEventBroadcaster(this);
		}

		/// <summary>
		/// Provides thread safety for the dictionary of settings.
		/// </summary>
		readonly ReaderWriterLockSlim settingsLockSlim = new ReaderWriterLockSlim();

		readonly Dictionary<string, object> cache = new Dictionary<string,object>();

		public void ClearCache()
		{
			settingsLockSlim.EnterWriteLock();
			try
			{
				cache.Clear();
			}
			finally
			{
				settingsLockSlim.ExitWriteLock();
			}
		}
		
		public void RemoveCacheItem(string key)
		{
			string cacheKey = ArgumentValidator.AssertNotNull(key, "key");

			settingsLockSlim.EnterWriteLock();
			try
			{
				cache.Remove(cacheKey);
			}
			finally
			{
				settingsLockSlim.ExitWriteLock();
			}
		}

		public bool RemoveSetting(string key)
		{
			ArgumentValidator.AssertNotNull(key, "key");
			bool result = false;

			settingsLockSlim.EnterWriteLock();
			try
			{
				if (localStore.Status == SettingsStoreStatus.Ready)
				{
					result = localStore.Remove(key);
				}

				if (roamingStore != localStore && roamingStore != null && roamingStore.Status == SettingsStoreStatus.Ready)
				{
					result |= roamingStore.Remove(key);
				}

				if (transientStore != null && transientStore.Status == SettingsStoreStatus.Ready)
				{
					result |= transientStore.Remove(key);
				}

				cache.Remove(key);
			}
			finally
			{
				settingsLockSlim.ExitWriteLock();
			}

			return result;
		}

		public bool TryGetSetting(string key, Type settingType, out object setting)
		{
			bool settingExists;
			setting = GetSettingCore(key, settingType, null, out settingExists);
			return settingExists;
		}

		public bool TryGetSetting<TSettingType>(string key, out TSettingType setting)
		{
			bool settingExists;
			setting = (TSettingType)GetSettingCore(key, typeof(TSettingType), default(TSettingType), out settingExists);
			return settingExists;
		}

		public StorageLocation? GetSettingLocation(string key)
		{
			StorageLocation? result = null;

			settingsLockSlim.EnterReadLock();

			try
			{
				if (localStore.Status == SettingsStoreStatus.Ready && localStore.Contains(key))
				{
					result = StorageLocation.Local;
				}
				else if (localStore != roamingStore && roamingStore.Status == SettingsStoreStatus.Ready && roamingStore.Contains(key))
				{
					result = StorageLocation.Roaming;
				}
				else if (transientStore.Status == SettingsStoreStatus.Ready && transientStore.Contains(key))
				{
					result = StorageLocation.Transient;
				}
			}
			finally
			{
				settingsLockSlim.ExitReadLock();
			}

			return result;
		}

		public T GetSetting<T>(string key, T defaultValue)
		{
			ArgumentValidator.AssertNotNull(key, "key");

			return (T)(object)GetSetting(key, typeof(T), defaultValue);
		}

		public object GetSetting(string key, Type settingType, object defaultValue)
		{
			bool settingExists;
			var result = GetSettingCore(key, settingType, defaultValue, out settingExists);
			return result;
		}

		object GetSettingCore(string key, Type settingType, object defaultValue, out bool settingExists)
		{
			ArgumentValidator.AssertNotNull(key, "key");

			/* [DV] We first test if the setting type is IXmlConvertible. 
			 * If it isn't then it may mean that the type <c>object</c> has been passed in via an overload. 
			 * The type of the return value may be IXmlConvertible and hence we test the default value 
			 * as a secondary precaution. It means that in some edge scenario you may find a type that is passed 
			 * as the default value has implemented IXmlConvertible whereas the stored value doesn't implement it. 
			 * If you're reading this and this happened to you, 
			 * send me an email danielvaughan@outcoder.com because that is curious. */
			bool xmlConvertible = typeof(IXmlConvertible).IsAssignableFrom(settingType);
			xmlConvertible |= settingType == typeof(object) && defaultValue is IXmlConvertible;

			bool returningDefaultValue;
			object result;

			settingsLockSlim.EnterReadLock();
			try
			{
				result = GetSettingFromStore(key, xmlConvertible, settingType, defaultValue, out returningDefaultValue);
			}
			finally
			{
				settingsLockSlim.ExitReadLock();
			}

			settingExists = !returningDefaultValue;

			return result;
		}

		object GetSettingFromStore(string key, bool xmlConvertible, 
			Type settingType, object defaultValue, out bool returningDefaultValue)
		{
			returningDefaultValue = false;
			string cacheKey = key;

			object cacheResult;
			if (cache.TryGetValue(cacheKey, out cacheResult))
			{
				return cacheResult;
			}

			object entry;

			if (localStore.Status == SettingsStoreStatus.Ready && localStore.TryGetValue(key, settingType, out entry)
				|| (roamingStore != null && roamingStore.Status == SettingsStoreStatus.Ready && roamingStore.TryGetValue(key, settingType, out entry)))
			{
				if (xmlConvertible && entry != null)
				{
					object objectConvertedFromXml;
					if (TryConvertFromXml(settingType, entry, out objectConvertedFromXml))
					{
						return objectConvertedFromXml;
					}
				}

				/* Android and perhaps other platforms don't have built in support for complex types.
				 * In the case where the entry is stored as a string but the setting type is not a string, 
				 * we attempt to convert the value.
				 * Here we deal with dates and resort to a type converter if all else fails. */
				Type entryType = entry?.GetType();

				if (entry != null && entryType != settingType
					&& settingType != typeof(string) && entryType == typeof(string))
				{
					string entryString = (string)entry;

					try
					{
						var stringEntry = (string)entry;
						if (settingType == typeof(DateTime))
						{
							DateTime d = DateTime.Parse(stringEntry);
							entry = d;
						}
						else if (settingType == typeof(DateTime?))
						{
							DateTime? d = null;
							if (!string.IsNullOrEmpty(stringEntry))
							{
								d = DateTime.Parse(stringEntry);
							}

							entry = d;
						}
						else if (settingType == typeof(DateTimeOffset))
						{
							DateTimeOffset d = DateTimeOffset.Parse(stringEntry);
							entry = d;
						}
						else if (settingType == typeof(DateTimeOffset?))
						{
							DateTimeOffset? d = null;
							if (!string.IsNullOrEmpty(stringEntry))
							{
								d = DateTimeOffset.Parse(stringEntry);
							}

							entry = d;
						}
						else if (entryString.StartsWith(SerializationContants.Base64EncodingPrefix))
						{
							int lengthOfPrefix = SerializationContants.Base64EncodingPrefix.Length;
							var dataPart = entryString.Substring(lengthOfPrefix, entryString.Length - lengthOfPrefix);
							entry = Convert.FromBase64String(dataPart);
						}
						else
						{
							var converter = new FromStringConverter(settingType);
							var convertedValue = converter.ConvertFrom(entry);
							entry = convertedValue;
						}
					}
					catch (Exception ex)
					{
						string message = $"Unable to parse setting that is of type {settingType} but is stored as a string";
						throw new Exception(message, ex);
					}
				}

				/* Dictionaries and other complex types can be difficult to serialize. 
				 * In some cases the SilverlightSerializer is used to serializer such objects.
				 */
				entry = InflateEntry(settingType, entry);

				cache[cacheKey] = entry;

				return entry;
			}

			if (transientStore.Status == SettingsStoreStatus.Ready
				&& transientStore.TryGetValue(key, settingType, out entry))
			{
				/* Dictionaries and other complex types can be difficult to serialize. 
				 * In some cases the SilverlightSerializer is used to serializer such objects.
				 */
				entry = InflateEntry(settingType, entry);

				cache[cacheKey] = entry;

				return entry;
			}

			returningDefaultValue = true;
			return defaultValue;
		}

		static object InflateEntry(Type settingType, object entry)
		{
			if (entry != null && entry.GetType() != settingType && settingType != typeof(byte[]) && entry is byte[])
			{
				try
				{
					var byteArray = (byte[])entry;
					entry = SilverlightSerializer.Deserialize(byteArray);
				}
				catch (Exception ex)
				{
					string message = string.Format("Unable to parse setting that is of type {0} but is stored as a byte[]", settingType);
					throw new Exception(message, ex);
				}
			}
			return entry;
		}

		bool TryConvertFromXml(Type settingType, object xmlFragment, out object result)
		{
			if (xmlFragment != null)
			{
				try
				{
					var convertible = (IXmlConvertible)Activator.CreateInstance(settingType);
					XElement element = XElement.Parse(xmlFragment.ToString());
					convertible.FromXElement(new XElement("Root", element));
					result = convertible;
					return true;
				}
				catch (Exception ex)
				{
					Debug.WriteLine("SettingsService: Unable to convert to IXmlConvertible. " + ex);

					if (RaiseExceptionsOnConversionErrors)
					{
						throw new Exception("SettingsService: Unable to convert to IXmlConvertible. " 
												+ settingType + " " + xmlFragment, ex);
					}
				}
			}

			result = null;
			return false;
		}

		public bool ContainsSetting<TSetting>(string key)
		{
			ArgumentValidator.AssertNotNull(key, "key");

			settingsLockSlim.EnterReadLock();
			try
			{
				string cacheKey = key;
				object cachedValue;
				if (cache.TryGetValue(cacheKey, out cachedValue) && cachedValue != null)
				{
					return true;
				}
				else
				{
					return ContainsSettingNotThreadSafe(key);
				}
			}
			finally
			{
				settingsLockSlim.ExitReadLock();
			}
		}

		public bool ContainsSetting(string key)
		{
			ArgumentValidator.AssertNotNull(key, "key");

			settingsLockSlim.EnterReadLock();
			try
			{
				return ContainsSettingNotThreadSafe(key);
			}
			finally
			{
				settingsLockSlim.ExitReadLock();
			}
		}

		bool ContainsSettingNotThreadSafe(string key)
		{
			bool hasSetting = localStore.Status == SettingsStoreStatus.Ready && localStore.Contains(key);
			hasSetting |= transientStore.Status == SettingsStoreStatus.Ready && transientStore.Contains(key);
			hasSetting |= localStore != roamingStore && roamingStore != null
				&& roamingStore.Status == SettingsStoreStatus.Ready && roamingStore.Contains(key);
			return hasSetting;
		}

		public SetSettingResult SetSetting<T>(string key, T value, StorageLocation storageLocation = StorageLocation.Local)
		{
			Type settingType = typeof(T);
			bool xmlConvertible = typeof(IXmlConvertible).IsAssignableFrom(settingType);

			/* Check to see if the value is already set. 
			 * This avoids raising events unnecessarily. */
			bool alreadySet;

			settingsLockSlim.EnterWriteLock();
			try
			{
				alreadySet = false;
				cache.Remove(key);

				object existingValue;
				if (localStore.Status == SettingsStoreStatus.Ready
						&& localStore.TryGetValue(key, settingType, out existingValue)
					|| (roamingStore != null && roamingStore.Status == SettingsStoreStatus.Ready
							&& roamingStore.TryGetValue(key, settingType, out existingValue)))
				{
					if (xmlConvertible)
					{
						object existingObject;
						if (TryConvertFromXml(settingType, existingValue, out existingObject))
						{
							if (object.Equals(existingObject, value))
							{
								alreadySet = true;
							}
						}
					}
					else if (existingValue != null && existingValue is byte[] && settingType != typeof(byte[]))
					{
						var existingObject = SilverlightSerializer.Deserialize((byte[])existingValue);
						if (AreEqual(value, existingObject))
						{
							alreadySet = true;
						}
					}

					if (!alreadySet && existingValue != null && existingValue.Equals(value))
					{
						alreadySet = true;
					}
				}

				if (!alreadySet && transientStore.Status == SettingsStoreStatus.Ready
						&& transientStore.TryGetValue(key, settingType, out existingValue)
						&& existingValue.Equals(value))
				{
					alreadySet = true;
				}
			}
			finally
			{
				settingsLockSlim.ExitWriteLock();
			}
			
			if (alreadySet)
			{
				return SetSettingResult.Successful;
			}

			/* Allows this action to be cancelled. */
			var args = new SettingChangingEventArgs(key, value);
			OnSettingChanging(args);

			if (args.Cancel)
			{
				Debug.WriteLine($"Setting change cancelled. Key: {key} New Value: {value}");
				return SetSettingResult.Cancelled;
			}

			settingsLockSlim.EnterWriteLock();
			try
			{
				string cacheKey = key;
				cache[cacheKey] = value;

				if (storageLocation == StorageLocation.Transient)
				{
					if (transientStore.Status == SettingsStoreStatus.Ready)
					{
						SetTransientStateValue(transientStore, key, value);
					}
					else
					{
						/* SettingsService is being used before app is completely initialized. */
						UISynchronizationContext.Instance.InvokeWithoutBlocking(delegate
						{
							if (transientStore.Status == SettingsStoreStatus.Ready)
							{
								SetTransientStateValue(transientStore, key, value);
							}
						});
					}
				}
				else
				{
					ISettingsStore store;
					if (storageLocation == StorageLocation.Roaming)
					{
						store = roamingStore ?? localStore;
					}
					else
					{
						store = localStore;
					}

					if (store.Status == SettingsStoreStatus.Ready)
					{
						SaveValueToStore(store, key, value, xmlConvertible, settingType);
					}
					else
					{
						/* We push the save value request onto the UI thread queue in case 
						 * some further initialization needs to occur. Otherwise it is up to 
						 * the ISettingsStore implementation to queue the update. */
						UISynchronizationContext.Instance.InvokeWithoutBlocking(delegate
						{
							if (store.Status == SettingsStoreStatus.Ready)
							{
								SaveValueToStore(store, key, value, xmlConvertible, settingType);
							}
						});
					}
				}
			}
			finally
			{
				settingsLockSlim.ExitWriteLock();
			}

			OnSettingChanged(new SettingChangeEventArgs(key, value));

			return SetSettingResult.Successful;
		}

//		void SetSettingAuxiliary<T>(string key, T value, Type settingType, 
//			bool xmlConvertible, out bool alreadySet)
//		{
//			alreadySet = false;
//			cache.Remove(key);
//
//			object existingValue;
//			if (localStore.Status == SettingsStoreStatus.Ready
//					&& localStore.TryGetValue(key, settingType, out existingValue)
//				|| (roamingStore != null && roamingStore.Status == SettingsStoreStatus.Ready
//						&& roamingStore.TryGetValue(key, settingType, out existingValue)))
//			{
//				if (xmlConvertible)
//				{
//					object existingObject;
//					if (TryConvertFromXml(settingType, existingValue, out existingObject))
//					{
//						if (object.Equals(existingObject, value))
//						{
//							alreadySet = true;
//							return;
//						}
//					}
//				}
//				else if (existingValue != null && existingValue is byte[] && settingType != typeof(byte[]))
//				{
//					var existingObject = SilverlightSerializer.Deserialize((byte[])existingValue);
//					if (AreEqual(value, existingObject))
//					{
//						alreadySet = true;
//						return;
//					}
//				}
//
//				if (existingValue != null && existingValue.Equals(value))
//				{
//					alreadySet = true;
//					return;
//				}
//			}
//
//			if (transientStore.Status == SettingsStoreStatus.Ready
//					&& transientStore.TryGetValue(key, settingType, out existingValue)
//					&& existingValue.Equals(value))
//			{
//				alreadySet = true;
//			}
//		}

		static bool AreEqual<T>(T value, object existingObject)
		{
			if (existingObject != null && existingObject is IDictionary && value is IDictionary)
			{
				if (CollectionComparer.DictionaryContentsAreEqualNonGeneric((IDictionary)existingObject, (IDictionary)value))
				{
					return true;
				}
			}
			else if (existingObject != null && existingObject is IList && value is IList)
			{
				if (CollectionComparer.ListsAreEqual((IList)existingObject, (IList)value))
				{
					return true;
				}
			}

			if (object.Equals(existingObject, value))
			{
				return true;
			}

			return false;
		}

		void SaveValueToStore<T>(ISettingsStore settingsStore, string key, T value, bool xmlConvertible, Type settingType)
		{
			object existingValue = null;
			bool valueRestorable = false;
			bool isNew = false;

			if (value == null)
			{
				settingsStore.Remove(key);
			}
			else
			{
				valueRestorable = settingsStore.TryGetValue(key, settingType, out existingValue);
				isNew = !valueRestorable;

				if (!xmlConvertible)
				{
					object savableValue = GetSavableValue(settingType, value);

					settingsStore[key] = savableValue;
				}
				else
				{
					object xmlRepresentationOfValue;
					try
					{
						IXmlConvertible convertible = (IXmlConvertible)value;
						var element = convertible.ToXElement();
						xmlRepresentationOfValue = element.ToString(SaveOptions.None);
					}
					catch (Exception ex)
					{
						Debug.WriteLine("SettingsService.SetSetting error raised converting to XML." + settingType + " " + ex);

						if (RaiseExceptionsOnConversionErrors)
						{
							throw new Exception(
								"SettingsService.SetSetting error raised converting to XML. Value:" + value + ", Setting type:" + settingType,
								ex);
						}

						xmlRepresentationOfValue = GetSavableValue(settingType, value);
					}

					settingsStore[key] = xmlRepresentationOfValue;
				}
			}

			try
			{
				/* We attempt to save the value to the settings store. 
				 * If the value cannot be saved, we attempt to roll back. */
				settingsStore.Save();
			}
			catch (Exception ex)
			{
				try
				{
					if (valueRestorable)
					{
						settingsStore[key] = existingValue;
					}
					else if (isNew)
					{
						settingsStore.Remove(key);
					}

					if (valueRestorable || isNew)
					{
						settingsStore.Save();
					}
				}
				catch (Exception ex2)
				{
					Debug.WriteLine("Unable to restore value after exception raised in SettingsService" + ex2);
				}

				if (RaiseExceptionsOnConversionErrors)
				{
					throw new Exception(
						"SettingsService.SetSetting error raised calling settingsStore.Save(). Value:" + value + ", Setting type:" + settingType,
						ex);
				}
			}
		}

		static void SetTransientStateValue<T>(ISettingsStore transientState, string key, T value)
		{
			if (value == null)
			{
				transientState.Remove(key);
			}
			else
			{
				Type settingType = typeof(T);

				object savableValue = GetSavableValue(settingType, value);

				transientState[key] = savableValue;
			}
		}

		static object GetSavableValue<T>(Type settingType, T value)
		{
			object savableValue = value;

			if (
#if WINDOWS_UWP || NETFX_CORE
				!settingType.GetTypeInfo().IsPrimitive
#else
				!settingType.IsPrimitive
#endif
				&& settingType != typeof(string) 
				&& settingType != typeof(DateTime) 
				&& settingType != typeof(Guid)
				&& settingType != typeof(byte[])
				//&& !settingType.IsSerializable 
				//&& !Attribute.IsDefined(settingType, typeof(DataContractAttribute))
				)
			{
				if (savableValue is DateTimeOffset)
				{
					var offset = (DateTimeOffset)savableValue;
					savableValue = offset.ToString(dateTimeFormatString);
				}
				else
				{
					savableValue = SilverlightSerializer.Serialize(value);
				}
			}

			return savableValue;
		}

		public async Task ClearSettings()
		{
			settingsLockSlim.EnterWriteLock();
			try
			{
				if (localStore.Status == SettingsStoreStatus.Ready)
				{
					await localStore.Clear();
					await localStore.Save();
				}

				if (roamingStore != null && roamingStore != localStore
					&& roamingStore.Status == SettingsStoreStatus.Ready)
				{
					await roamingStore.Clear();
					await roamingStore.Save();
				}

				if (transientStore.Status == SettingsStoreStatus.Ready)
				{
					await transientStore.Clear();
					await transientStore.Save();
				}
			}
			finally
			{
				settingsLockSlim.ExitWriteLock();
			}
		}

#region event SettingChanging

		public event EventHandler<SettingChangingEventArgs> SettingChanging;

		protected virtual void OnSettingChanging(SettingChangingEventArgs e)
		{
			SettingChanging?.Invoke(this, e);
		}

#endregion

#region event SettingChanged

		public event EventHandler<SettingChangeEventArgs> SettingChanged;

		protected virtual void OnSettingChanged(SettingChangeEventArgs e)
		{
			SettingChanged?.Invoke(this, e);
		}

#endregion
	}
}
